package funcparser

import (
	"fmt"
	"strconv"
	"strings"
)

// Type Function represents a mathematical function from float64 to float64.
type Function interface {
	Eval(x float64) float64
}

// Main function of this module. Given a function description in a string it
// returns either a Function evaluating to the corresponding passed description
// or an error if a problem occurred while parsing. 
func Parse(text string) (Function, error) {
	tokens := Tokenize(strings.Trim(text, "'"))
	p := peekerChannel{c: tokens}
	f := parseExp(&p)
	checkEq(p.next(), EOF, "")
	return f, nil
}

// Actual implementation of the Function for this parser. It is actually just a
// unary function on float64 to float64.
func (f unaryFunc) Eval(x float64) float64 {
	return f(x)
}

// Helper function so we can get the pointer of a value obtained from a channel.
func pointer(t Token) *Token {
	aux := t
	return &aux
}

// Helper function checking equality for tokens and emmiting panic when
// different.
func checkEq(t *Token, typ tokenType, text string) {
	if !t.Equals(typ, text) {
		panic(fmt.Sprintf("Expecting {%v, '%v'}, but found %#v.", typ, text, t))
	}
}

// Creates a new function as the result of applying a binary operator to the
// result of evaluating the two given functions.
// The order is respected, which may matter for asymetric ops such division.
func applyBinop(op binaryFunc, a, b *unaryFunc) *unaryFunc {
	f := unaryFunc(func(x float64) float64 {
		return op(a.Eval(x), b.Eval(x))
	})
	return &f
}

// Helper type allowing to peek into a channel without consuming the token.
type peekerChannel struct {
	c      <-chan Token
	peeked *Token
}

// Returns the address of the Token that next() will return next time it's
// called. Consecutive calls will return the same value.
func (p *peekerChannel) peek() *Token {
	if p.peeked == nil {
		aux := <-p.c
		p.peeked = &aux
	}
	return p.peeked
}

// Returns the address of the next Token in the channel, which may or may not
// have been peeked before.
func (p *peekerChannel) next() *Token {
	aux := p.peek()
	p.peeked = nil
	return aux
}

// Parses an atomic value corresponding to the following grammar rule.
// value = ( exp )
//       | INTEGER
//       | FLOAT
//       | id
//       | id ( exp )
//       | - value 
func parseValue(tokens *peekerChannel) *unaryFunc {
	t := tokens.next()

	switch {

	// Opening parenthesis
	case t.Equals(op, "("):
		f := parseExp(tokens)
		checkEq(tokens.next(), op, ")")
		return f

	// A literal integer
	case t.TypeIs(integer):
		f := unaryFunc(func(x float64) float64 {
			i, _ := strconv.Atoi(t.text)
			return float64(i)
		})
		return &f

	// A literal float
	case t.TypeIs(float):
		f := unaryFunc(func(x float64) float64 {
			f, _ := strconv.ParseFloat(t.text, 64)
			return f
		})
		return &f

	// An id: "x" or a function call
	case t.TypeIs(id):
		if t.TextIs("x") {
			f := unaryFunc(func(x float64) float64 { return x })
			return &f
		} else {
			if f, found := funcs[t.text]; found {
				checkEq(tokens.next(), op, "(")
				exp := parseExp(tokens)
				checkEq(tokens.next(), op, ")")
				f := unaryFunc(func(x float64) float64 {
					return f(exp.Eval(x))
				})
				return &f
			}
			panic(fmt.Sprintf("Function %s not defined.", t.text))
		}

	// Unary operator -
	case t.Equals(op, "-"):
		v := parseValue(tokens)
		f := unaryFunc(func(x float64) float64 {
			return funcs["-"](v.Eval(x))
		})
		return &f

	default:
		panic(fmt.Sprintf("Unexpected token %v.", t))
	}
	panic("unreachable1")
}

// Parses a term corresponding to the following grammar rule.
// term = value ([*/%] value)*
func parseTerm(tokens *peekerChannel) *unaryFunc {
	v := parseValue(tokens)
	for !tokens.peek().TypeIs(EOF) {
		p := tokens.peek()
		if p.TypeIs(op) {
			if strings.Contains(termops, p.text) {
				tokens.next()
				u := parseValue(tokens)
				v = applyBinop(binops[p.text], v, u)
			} else {
				// Non term-operator found, maybe it's an expression.
				return v
			}
		} else {
			panic(fmt.Sprintf("Expecting operator, but found %#v.", p))
		}
	}
	return v
}

// Parses an expression corresponding to the following grammar rule.
// exp = term ([+-] term)*
func parseExp(tokens *peekerChannel) *unaryFunc {
	t := parseTerm(tokens)
	for !tokens.peek().TypeIs(EOF) {
		p := tokens.peek()
		if p.TypeIs(op) && strings.Contains(expops, p.text) {
			tokens.next()
			u := parseTerm(tokens)
			t = applyBinop(binops[p.text], t, u)
		} else {
			return t
		}
	}
	return t
}
